import os
import collections
import datetime
import json

from coin.base.datetime_util import to_timestamp_int
from coin.base.param_util import to_list

from coin.exchange.kr_rest.product_holders import get_holder_from_product

from coin.strategy.hk_hard_arb.index_pricer import IndexPricer
from coin.strategy.hk_hard_arb.pricer import PositionLeanPricer
from coin.strategy.hk_hard_arb.util import clean_json
from coin.strategy.hk_hard_arb.unhedged_mm_3 import PassUnhedgeStrategyConfig

import coin.strategy.executor.aggressive.util as autil
import coin.strategy.executor.passive.util as putil

import coin.strategy.mm.base.bookmap as bm
import coin.strategy.mm.fastfeature.feed_manager as fmgr
import coin.strategy.mm.fastfeature.fast_feature_feed_spec as fspec
import coin.strategy.mm.fastfeature.sim.lm_strategy_sim as lmssim
import coin.strategy.mm.fastfeature.sim.lm_sim_executor as lmsimexec
import coin.strategy.mm.fastfeature.sim.lm_strategy_logic as lmlogic
import coin.strategy.mm.fastfeature.linear_model_factory as lmf
import coin.strategy.mm.fastfeature.util as ffutil
import coin.strategy.mm.fastfeature.feature_feed_converter as ffcv
import coin.strategy.mm.tool.archive_base as abase
import coin.strategy.mm.subscription as sub

import coin.proto.archive_flag_pb2 as afpb
from coin.support.latency.latency_model import LatencyModel

import pyfastfeature.feature.simulator as pysim


class UnhedgedSimStrategy(lmssim.LmSimStrategy):
  ExecutorType = lmsimexec.LmSimMMExecutor
  ExecutorConfigConverter = putil.convert_executor_config_from_dict
  PostOnly = True

  def __init__(self,
               *,
               logger,
               model_profile,
               trade_product,
               latency_ms,
               use_post,
               use_feed_cache,
               edge_bp,
               basis_ma_window_min,
               start_time,
               end_time):
    self._start_time = to_timestamp_int(start_time)
    self._end_time = to_timestamp_int(end_time)
    print(self._start_time, self._end_time)
    self._logger = logger

    config = model_profile.config_dict
    self._config = config
    self._trade_subreq = model_profile.trade_subreq
    self._trade_product = model_profile.trade_product
    self._ref_subreq = model_profile.ref_subreq
    self._ref_product = model_profile.ref_product
    assert len(self._ref_subreq) == len(self._ref_product)

    self._edge = None
    self._spread = None
    self._pull_edge = None

    self._bbo_protection = config.get('bbo_protection', False)
    if edge_bp is not None:
      config['edge_bp'] = edge_bp
    if 'edge_bp' in config:
      self._edge = config['edge_bp'] / 10000.
    if 'spread_bp' in config:
      self._spread = config['spread_bp'] / 10000.
    if 'pull_edge_bp' in config:
      self._pull_edge = config['pull_edge_bp'] / 10000.
    if 'pull_spread_bp' in config:
      self._pull_edge = config['pull_spread_bp'] / 10000. / 2

    if 'fill_widen_within_sec' in config:
      self._fill_widen_within = config['fill_widen_within_sec'] * 1e9
      self._fill_widen = config['fill_widen_bp'] / 10000.
    else:
      self._fill_widen_within = None
      self._fill_widen = None

    if basis_ma_window_min is not None:
      config['basis_ma_window_min'] = basis_ma_window_min

    assert 1 == sum([
        self._edge is not None,
        self._spread is not None,
    ])

    products = self._ref_product + to_list(self._trade_product)
    self._bookmap = bm.BookMap(products, logger=self._logger)

    tick = get_holder_from_product(self._trade_product).product_info.gen_tick()

    self._pricer = IndexPricer(self._config, tick, use_fastfeed=True)
    self._pos_lean_pricer = PositionLeanPricer(min_pos=config['min_pos'],
                                               max_pos=config['max_pos'],
                                               lot_size=config['lot_size'],
                                               lean_edge_bp=config.get('lean_edge_bp', 0))

    self._executor = None
    self._exit = False

    psub_map = {}
    for feed_product in model_profile.ref_product + [model_profile.trade_product]:
      sub_key = sub.get_subscription_key(feed_product,
                                         api_version_override_map={('Futures', 'Okex'): 'v3'})
      psub_map[feed_product] = sub_key.to_str()

    self._product_map = ffutil.product_map_from_products(list(psub_map.keys()))
    self._feed_products = list(self._product_map.keys())

    self._feedspec = fspec.FastFeatureFeedInfo(
        psub_map=psub_map,
        level_override_map=collections.defaultdict(lambda: 30),
        sub_key_not_none=False,
        bitmex_bbo_compress=False)

    ffcv_converter = ffcv.FeatureFeedCacheConverter if use_feed_cache else ffcv.FeatureArchiveFeedConverter
    self._feed_converter = lmf.create_feed_converter(ffcv_converter,
                                                     self._product_map,
                                                     self._feedspec,
                                                     start_time)

    self._feed_manager = fmgr.FeedManager(self.on_book, self.on_trade, {}, feedspec=self._feedspec)

    self._simulator = pysim.Simulator(list(self._product_map.values()),
                                      latency_ns=int(latency_ms * 1e6),
                                      delay_accept_until_book=False)

    self._feed_converter.register_symbols(self._simulator)
    self._trade_pindex = self._feed_converter.get_index(trade_product)

    order_gateway = lmsimexec.LmSimOrderGateway(self._logger,
                                                self._simulator,
                                                trade_product,
                                                self._trade_pindex,
                                                post_only=self.PostOnly)

    self._execution_params = type(self).ExecutorConfigConverter(trade_product.symbol, self._config)

    self._executor = self.ExecutorType(trade_product,
                                       self._execution_params,
                                       order_gateway,
                                       self._logger)

  def get_misc_info(self):
    return None

  def on_book_reset(self, book_builder_name, book_builder):
    self._feed_manager.set_book_reset(self._feed_products, book_builder_name, book_builder)

  def on_trade(self, product, trade):
    if not (self._start_time < trade.timestamp < self._end_time):
      return
    self._feed_converter.update_trade(product, trade, self._simulator)

  def on_book(self, product, book):
    if not (self._start_time < book.timestamp < self._end_time):
      return
    self._feed_converter.update_books(product, book, [self._simulator])
    if self._exit:
      return

    timestamp = book.timestamp

    self._executor.set_timestamp(timestamp)
    self._executor.manage2_mm_orders()

    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return

    ref_book = [self._bookmap.get_book(product) for product in self._ref_product]
    trade_book = self._bookmap.get_book(self._trade_product)
    self._pricer.update(ref_book, trade_book)

    if not self._pricer.ready or self._executor is None:
      return

    if self._spread is not None:
      askp, bidp = self._pricer.calculate_price_by_spread(self._spread,
                                                          bbo_protection=self._bbo_protection)
    elif self._edge is not None:
      askp, bidp = self._pricer.calculate_price(self._edge,
                                                self._edge,
                                                bbo_protection=self._bbo_protection)
    else:
      raise ValueError('not supported mode')

    pos = self._executor.query_unbiased_position()
    askp, bidp = self._pos_lean_pricer.update(pos=pos, buy_price=bidp, sell_price=askp)

    if self._fill_widen_within and timestamp - self._executor._lfillt._ts._last_timestamp < self._fill_widen_within:
      askp = askp * (1 + self._fill_widen)
      bidp = bidp * (1 - self._fill_widen)

    askp, bidp = self._pricer.adjust_price(askp_1=askp,
                                           bidp_1=bidp,
                                           bbo_protection=self._bbo_protection)

    sell_price_pull = None
    buy_price_pull = None
    if askp is not None and self._pull_edge is not None:
      sell_price_pull = askp * (1 - self._pull_edge)
    if bidp is not None and self._pull_edge is not None:
      buy_price_pull = bidp * (1 + self._pull_edge)

    self._executor.submit_mm_orders(sell_price=askp,
                                    buy_price=bidp,
                                    sell_price_pull=sell_price_pull,
                                    buy_price_pull=buy_price_pull,
                                    timestamp=timestamp)


class UnhedgedMultiSimStrategy(object):
  def __init__(self,
               strat_config,
               start_time,
               end_time,
               latency_ms,
               use_post,
               is_agg,
               use_feed_cache,
               edge_bp,
               basis_ma_window_min,
               use_predicted_latency,
               logger):
    assert isinstance(strat_config, PassUnhedgeStrategyConfig)

    if use_predicted_latency:
      model = LatencyModel()
      path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                          '../../../../support/latency/20200103.model')
      model.load(path)
      try:
        latency_ms = int(
            model.predict({
                'exchange': strat_config._trade_subreq.exchange,
                'market_type': strat_config._trade_subreq.market_type
            }) / 1e6)
        print('predicted latency: ',
              latency_ms,
              'ms for ',
              strat_config._trade_subreq.exchange,
              strat_config._trade_subreq.market_type)
      except Exception:
        pass

    self.substrats = {}
    for name, profile in strat_config.model_profile.items():
      self.substrats[name] = UnhedgedSimStrategy(model_profile=profile,
                                                 trade_product=profile.trade_product,
                                                 latency_ms=latency_ms,
                                                 use_post=use_post,
                                                 start_time=start_time,
                                                 end_time=end_time,
                                                 use_feed_cache=use_feed_cache,
                                                 edge_bp=edge_bp,
                                                 basis_ma_window_min=basis_ma_window_min,
                                                 logger=logger)

  def on_book_reset(self, book_builder_name, book_builder):
    for substrat in self.substrats.values():
      print(book_builder_name, book_builder)
      substrat.on_book_reset(book_builder_name, book_builder)

  def get_products(self):
    products = []
    for substrat in self.substrats.values():
      products.extend(substrat._feed_products)
    return list(set(products))

  def get_result(self):
    ret_dict = {}
    for name, substrat in self.substrats.items():
      ret_dict[name] = (substrat._trade_product,
                        substrat._simulator.GetHistoricalResponse(substrat._trade_pindex),
                        substrat.get_misc_info())
    return ret_dict


def get_ref_subreq_and_product_from_ref_feed_symbol(ref_feed_symbol):
  # Form: Okex.Futures.v3_swap:BTC-USD.PERPETUAL
  # Form: Okex.Futures:BTC-USD.QUARTER
  ref_subreq = (ref_feed_symbol.split(':')[0]).split('.')
  symbol = ref_feed_symbol.split(':')[1]
  subreq_dict = {'exchange': ref_subreq[0], 'market_type': ref_subreq[1]}
  if len(ref_subreq) == 3:
    subreq_dict['api_version'] = ref_subreq[2]
  return subreq_dict, symbol


def get_start_end_time(trading_date, time_range):
  add_hour, hours = time_range.split("-")
  add_hour = float(add_hour)
  hours = float(hours) - add_hour
  start_time = trading_date + datetime.timedelta(hours=add_hour)
  end_time = start_time + datetime.timedelta(hours=hours)
  return start_time, end_time


def get_strategy_config(config_filename,
                        trading_date,
                        time_range,
                        ref_feed_symbol,
                        *,
                        pick_symbol=None,
                        neutral=True,
                        lean_edge_bp=None,
                        order_update_period_sec=None,
                        **sink_kwargs):
  if trading_date is None:
    start_time = None
    end_time = None
  else:
    start_time, end_time = get_start_end_time(trading_date, time_range)
  conftext = open(config_filename).read()
  confparsed = conftext.split("echo \'")[-1].split("\')")[0]
  mmconf = json.loads(clean_json(confparsed))
  if pick_symbol is not None and len(pick_symbol) > 0:
    mmconf['products'] = {
        key: value for key, value in mmconf['products'].items() if key.find(pick_symbol) >= 0
    }

  if ref_feed_symbol is not None:
    subreq_dict, ref_symbol = get_ref_subreq_and_product_from_ref_feed_symbol(ref_feed_symbol)
    mmconf['common']['ref_feed_subreqs'] = [subreq_dict]
    for product, prop in mmconf['products'].items():
      prop['ref_symbol'] = [ref_symbol]

  if order_update_period_sec is not None and order_update_period_sec != 'None':
    mmconf['common']['order_update_period_sec'] = float(order_update_period_sec)
    mmconf['common']['min_posting_period_sec'] = min(mmconf['common']['min_posting_period_sec'],
                                                     float(order_update_period_sec))

  for product, prop in mmconf['products'].items():
    if neutral:
      reserve = 0.5 * (prop['max_pos'] + prop['min_pos'])
      prop['max_pos'] -= reserve
      prop['min_pos'] -= reserve
    if lean_edge_bp is not None and lean_edge_bp != 'None':
      prop['lean_edge_bp'] = float(lean_edge_bp)

  strat_config = PassUnhedgeStrategyConfig(mmconf, start_time)
  return strat_config


def dump_unhedged_sim(logger,
                      config_filename,
                      trading_date,
                      time_range,
                      latency_ms,
                      basis_ma_window_min,
                      ref_feed_symbol,
                      use_predicted_latency,
                      *,
                      edge_bp=None,
                      use_post=None,
                      **sink_kwargs):
  strat_config = get_strategy_config(config_filename,
                                     trading_date,
                                     time_range,
                                     ref_feed_symbol,
                                     **sink_kwargs)
  fsflags = abase.get_feed_subscription_flags()
  start_time, end_time = get_start_end_time(trading_date, time_range)

  dumpstrategy = UnhedgedMultiSimStrategy(
      strat_config,
      start_time=start_time,
      end_time=end_time,
      latency_ms=latency_ms,
      use_post=use_post,
      is_agg=False,
      use_feed_cache=fsflags.use_feed_cache,
      edge_bp=edge_bp,
      basis_ma_window_min=basis_ma_window_min,
      use_predicted_latency=use_predicted_latency,
      logger=logger,
  )

  baseflags = afpb.BaseFlagProto(trading_date=trading_date.strftime("%Y%m%d"),
                                 time_range=time_range)

  fs_req = sub.FeedSubscriptionRequest()

  for x in strat_config.model_profile.values():
    fs_req.add_products(x.ref_product, x.ref_subreq[0])
    fs_req.add_products([x.trade_product], x.trade_subreq)

  abase.run_from_archive_base_with_feed_sub(baseflags=baseflags,
                                            feed_sub_flags_proto=fsflags,
                                            feed_sub_request=fs_req,
                                            on_book_reset=dumpstrategy.on_book_reset)

  return dumpstrategy.get_result()
